/**
 * This file is released under the GNU General Public License.
 * Refer to the COPYING file distributed with this package.
 *
 * Copyright (c) 2008-2009 WURFL-Pro srl
 */
package net.sourceforge.wurfl.core.resource;

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import net.sourceforge.wurfl.core.CapabilityNotDefinedException;
import net.sourceforge.wurfl.core.Constants;
import net.sourceforge.wurfl.core.DeviceNotDefinedException;
import net.sourceforge.wurfl.core.GroupNotDefinedException;

import org.apache.commons.collections.iterators.ReverseListIterator;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;

/**
 * This is the default {@link WURFLModel} implementation. This class store the
 * devicesById in a Map in memory so it is very quick and no need for cache.
 * 
 * @author WURFL-PRO SRL, Rome, Italy
 * @version $Id: DefaultWURFLModel.java 1045 2009-03-09 15:52:53Z filippo.deluca $
 */
public class DefaultWURFLModel implements WURFLModel {

	/** The held devices by id Map */
	private Map/* String,ModelDevice */devicesById;

	private String version;

	/** log */
	private static final Log log = LogFactory.getLog(DefaultWURFLModel.class);

	// Constructors *******************************************************

	/**
	 * Build a model by WURFLResource.
	 * 
	 * @param root
	 *            resource represents the wurfl main resource.
	 * @throws WURFLConsistencyException
	 *             if the model is not consistent.
	 */
	public DefaultWURFLModel(WURFLResource root) {

		this(root, new WURFLResources());
	}

	/**
	 * Build model by root WURFLResource and patches WURFLResources.
	 * 
	 * @param root
	 *            resource represents the wurfl main resource.
	 * @param patches
	 *            resources represent the wurfl patches to apply.
	 * @throws WURFLConsistencyException
	 *             if the model is not consistent.
	 */
	public DefaultWURFLModel(WURFLResource root, WURFLResources patches) {

		Validate.notNull(root, "The root resource must be not null.");

		ResourceData rootData = root.getData();
		this.version = rootData.getInfo();

		// Defensive copy
		ModelDevices rootDevices = rootData.getDevices();
		ModelDevices tempDevices = new ModelDevices(rootDevices);

		if (log.isDebugEnabled()) {
			log.debug(tempDevices.size() + " devices found in "
					+ rootData.getInfo());
		}

		if (log.isDebugEnabled()) {
			WURFLConsistencyVerifier.verify(tempDevices);
		}

		// Applies patches
		for (int patchIndex = 0; patches != null && patchIndex < patches.size(); patchIndex++) {
			WURFLResource patchResource = patches.get(patchIndex);
			ResourceData patchData = patchResource.getData();

			ModelDevices patchDevices = patchData.getDevices();

			if (log.isDebugEnabled()) {
				log.debug(patchDevices.size() + " devices found in "
						+ patchData.getInfo());
			}

			StrBuilder versionBuilder = new StrBuilder();
			versionBuilder.append(StringUtils.defaultString(version)).append(
					"; ").append(patchData.getInfo());

			tempDevices = WURFLPatchingManager.patchDevices(tempDevices,
					patchDevices);

			if (log.isDebugEnabled()) {
				log
						.debug("Verifing device after patch: "
								+ patchData.getInfo());
				WURFLConsistencyVerifier.verify(tempDevices);
			}

			version = versionBuilder.toString();

		}

		log.debug("Verifing building devicesById");
		WURFLConsistencyVerifier.verify(tempDevices);

		devicesById = new ConcurrentHashMap(tempDevices.getDevicesById());

		if (log.isInfoEnabled()) {

			log.info("WURFLModel version: " + version + "; devices: "
					+ devicesById.size());
		}

	}

	// WURLModel implementation *******************************************

	/**
	 * Returns the version of backed WURFL repository.
	 * <p>
	 * The version string may change by Resource to Resource. For XMLResource
	 * the version string is: <code>fileType:filePath:fileVersion</code> where
	 * fileType my be Root or Patch.
	 * </p>
	 * 
	 * <p>
	 * The version describe all resources loaded by the model, each resource is
	 * divided by &quot;;&quot;<br />
	 * ex:
	 * <code>Root:/WEB-INF/wurfl.zip:1.2;Patch:/WEB-INF/patch_spv.xml:1.4;Patch:/WEB-INF/patch_web_browser.xml:1.0</code>
	 * </p>
	 * 
	 * @return String representing version of underlying WURFL repository.
	 */
	public String getVersion() {
		return version;
	}

	public ModelDevice getDeviceById(String id)
			throws DeviceNotDefinedException {

		Validate.notEmpty(id, "The id must be not null");

		ModelDevice modelDevice = (ModelDevice) devicesById.get(id);

		if (modelDevice == null) {
			throw new DeviceNotDefinedException(id);
		}

		return (ModelDevice) devicesById.get(id);
	}

	public Set getDevices(Set devicesIds) throws DeviceNotDefinedException {

		Validate.notNull(devicesIds, "The devicesIds must be not null Set");
		Validate.noNullElements(devicesIds,
				"The devicesIds must not containing null elements");
		Validate.allElementsOfType(devicesIds, String.class,
				"The devicesIds must containing right devicesById id");

		Set resultDevices = new HashSet();

		for (Iterator it = devicesIds.iterator(); it.hasNext();) {
			resultDevices.add(getDeviceById((String) it.next()));
		}

		return resultDevices;
	}

	public Set getAllDevices() {

		HashSet returningDevices = new HashSet();
		returningDevices.addAll(devicesById.values());

		return returningDevices;
	}

	public Set getAllDevicesId() {

		HashSet returningIds = new HashSet();
		returningIds.addAll(devicesById.keySet());

		return returningIds;
	}

	public List getDeviceHierarchy(ModelDevice device)
			throws DeviceNotInModelException {

		Validate.notNull(device, "The device must be not null");

		LinkedList hierarchy = new LinkedList();
		ModelDevice looper = device;

		// WARNING generic -> ... -> root
		while (!Constants.GENERIC.equals(looper.getID())) {
			hierarchy.addFirst(looper);
			looper = getDeviceFallback(looper);
		}

		hierarchy.addFirst(looper);

		return hierarchy;

	}

	public ModelDevice getDeviceFallback(ModelDevice device)
			throws DeviceNotInModelException {

		Validate.notNull(device, "The device must be not null");

		ModelDevice fallback = null;

		try {
			fallback = getDeviceById(device.getFallBack());
		} catch (DeviceNotDefinedException e) {
			throw new DeviceNotInModelException(device);
		}

		return fallback;
	}

	public ModelDevice getDeviceAncestor(ModelDevice device)
			throws DeviceNotInModelException {

		Validate.notNull(device, "The device must be not null");

		ModelDevice ancestor = device;
		ModelDevice generic = getGenericDevice();

		List hierarchy = getDeviceHierarchy(device);
		ReverseListIterator hit = new ReverseListIterator(hierarchy);

		while (hit.hasNext() && !ancestor.isActualDeviceRoot()
				&& !generic.equals(ancestor)) {
			ancestor = (ModelDevice) hit.next();
		}

		// The model is invalid
		if (!ancestor.isActualDeviceRoot() && !generic.equals(ancestor)) {
			throw new RuntimeException("Hierarchy is invalid");
		}

		return ancestor;
	}

	public boolean isDeviceDefined(String deviceId) {

		Validate.notEmpty(deviceId, "The deviceId must be not null");

		return devicesById.containsKey(deviceId);
	}

	public int size() {

		return devicesById.size();
	}

	// Groups methods *****************************************************

	public Set getAllGroups() {

		ModelDevice generic = getGenericDevice();

		return generic.getGroups();
	}

	public boolean isGroupDefined(String groupId) {

		Validate.notEmpty(groupId, "The groupId must be not null");

		ModelDevice generic = getGenericDevice();

		return generic.defineGroup(groupId);
	}

	public String getGroupByCapability(final String capabilityName)
			throws CapabilityNotDefinedException {

		Validate
				.notEmpty(capabilityName, "The capabilityName must be not null");

		ModelDevice generic = getGenericDevice();

		if (!generic.defineCapability(capabilityName)) {
			throw new CapabilityNotDefinedException(capabilityName);
		}

		return generic.getGroupForCapability(capabilityName);
	}

	// Capabilities methods ***********************************************

	public Set getAllCapabilities() {

		ModelDevice generic = getGenericDevice();

		return new HashSet(generic.getCapabilities().keySet());
	}

	public boolean isCapabilityDefined(String capability) {

		Validate.notEmpty(capability, "The capability must be not null");

		ModelDevice generic = getGenericDevice();

		return generic.defineCapability(capability);
	}

	public Set getCapabilitiesForGroup(final String groupId)
			throws GroupNotDefinedException {

		Validate.notEmpty(groupId, "The groupId must be not null");

		ModelDevice generic = getGenericDevice();

		if (!generic.defineGroup(groupId)) {
			throw new GroupNotDefinedException(groupId);
		}

		return generic.getCapabilitiesNamesForGroup(groupId);
	}

	public ModelDevice getDeviceWhereCapabilityIsDefined(
			ModelDevice rootDevice, String name)
			throws DeviceNotInModelException, CapabilityNotDefinedException {

		Validate.notNull(rootDevice, "The rootDevice must be not null");
		Validate.notEmpty(name, "The name must be not null");

		List hierarchy = getDeviceHierarchy(rootDevice);
		for (ListIterator hIt = new ReverseListIterator(hierarchy); hIt
				.hasNext();) {

			ModelDevice looper = (ModelDevice) hIt.next();

			if (looper.defineCapability(name)) {
				return looper;
			} else if (Constants.GENERIC.equals(looper.getID())) {
				throw new CapabilityNotDefinedException(name);
			}

		}

		// It is impossible because the devicesById has verified
		throw new RuntimeException(new OrphanHierarchyException(hierarchy));

	}

	// Support methods ****************************************************

	protected ModelDevice getGenericDevice() {

		ModelDevice generic = (ModelDevice) devicesById.get(Constants.GENERIC);

		// it is impossible because the devicesById are verified
		if (generic == null && devicesById.size() > 0) {
			throw new RuntimeException(new GenericNotDefinedException());
		}

		return generic;
	}

	// Commons methods ****************************************************

	public String toString() {
		ToStringBuilder tb = new ToStringBuilder(this);

		tb.append(version);

		return tb.toString();
	}

}
